Skip to content

Go 结构体与方法 ​

结构体把相关数据放在一起,方法把围绕这些数据的操作放在类型上。真正容易写错的地方,是接收者到底拿到哪一份数据:func (c Counter) 与 func (c *Counter) 只差一个星号,却会影响修改是否留下来、接口能否实现,以及保存方法后会读到哪个值。

这篇从一个计数器开始,依次看接收者、方法集、嵌入、标签和内存布局。先把容易混在一起的几件事拆开:

要决定什么看哪个机制本篇会验证的现象
方法能否改到原结构体字段值接收者、指针接收者改结构体副本的整数不生效,改共享 slice 的元素却生效
某类型能否放进接口方法集c.Inc() 能调用,不代表 Counter 实现了 Inc() 接口
能否少写一层字段名嵌入与提升s.Label() 可以来自内嵌类型,但不会自动调用外层“覆盖”的方法
JSON 名称和空字段怎么处理struct tag 与消费它的包标签只是元数据,validate:"required" 不会自行检查
同样字段为什么占用不同字节对齐与填充换字段顺序,本机结构体大小从 24 字节变成 16 字节

正文输出在 Go 1.27.2、Linux/amd64 上实际运行。方法与类型关系是语言规则;具体布局数字只对应这个工具链和架构。完整代码和反例都在文末。

先有结构体,再给它定义操作 ​

go
type Counter struct {
    N       int
    Samples []int
}

Counter 是一个定义类型,里面有两个字段。字段名首字母大写可以从其他包访问,未导出的字段则限于所属包。结构体字面量可以按字段名赋初值,没写出的字段取零值。[1][2]

写法得到什么需要注意什么
var c Counter零值结构体N 为 0,Samples 为 nil
Counter{N: 1}一个结构体值没写的 Samples 仍为 nil
&Counter{N: 1}指向结构体的 *Counter可以通过指针修改原字段
new(Counter)指向零值结构体的 *Counter不会替你填业务默认值
var p *Counternil 指针没有自动创建一个 Counter

这些创建方式的区别来自零值、复合字面量和 new 的规则。&Counter{}、new(Counter) 不意味着“肯定在堆上分配”;这里先讨论类型和可观察行为,具体存储位置交给编译器分析,不由一个 & 决定。[3][4]

方法的接收者写在方法名前面。接收者类型要满足规范要求,其基类型必须在同一个包中定义,不能随手给另一个包的类型追加方法。func (c Counter) Value() int 表示这个操作属于 Counter,调用时用 c.Value()。[5]

值接收者与指针接收者:复制的是结构体,还是指针 ​

给计数器加三个方法:

go
func (c Counter) TryChange() {
    c.N++
    c.Samples[0]++
}
func (c Counter) Value() int { return c.N }
func (c *Counter) Inc()      { c.N++ }

两类接收者可以先按同一组问题比较:[5:1][6]

问题值接收者 func (c Counter)指针接收者 func (c *Counter)
调用时复制什么结构体值,各字段按值复制一个指针值
给 c.N 赋值改接收者副本改指针指向的结构体
修改字段中的共享 slice/map 内容仍可能影响外部数据也可能影响外部数据
能否当作“只读方法”的保证不能,引用数据仍可能变不能
通常适合什么小、可复制、按值理解的对象需要修改原对象,或不应复制的对象

值接收者不等于深复制,也不等于只读 ​

go
c := Counter{N: 1, Samples: []int{10}}
c.TryChange()
fmt.Println(c.N, c.Samples)
c.Inc()
fmt.Println(c.N)
text
1 [11]
2

TryChange 拿到结构体副本,副本的 N 从 1 加到了 2,但原对象的 N 留在 1。Samples 字段是 slice,复制结构体只是复制了 slice 描述,所以两份字段仍指向同一个数组;改第一个样本,原对象就看到了 11。之后的 Inc 拿到原结构体指针,才把原来的 N 改成 2。[6:1]

正在绘制图示…

可切换适合窗口或原始尺寸
查看图源代码
flowchart LR
    O["调用方 c<br/>N = 1<br/>Samples 描述"] --> A["共享数组<br/>11"]
    V["TryChange 的副本<br/>N = 2<br/>Samples 描述"] --> A
    P["Inc 的接收者<br/>复制的指针"] --> O
放大图示

原始尺寸下可横向或纵向滚动;按 Escape 关闭。

正在绘制图示…

查看图源代码
flowchart LR
    O["调用方 c<br/>N = 1<br/>Samples 描述"] --> A["共享数组<br/>11"]
    V["TryChange 的副本<br/>N = 2<br/>Samples 描述"] --> A
    P["Inc 的接收者<br/>复制的指针"] --> O

给结构体换一个值接收者,不能自动获得并发安全。如果方法通过 slice、map 或指针字段修改共享对象,仍然要按共享读写处理。

怎么选接收者 ​

需要修改对象状态时,指针接收者最直接。结构体含有 sync.Mutex 等使用后不可复制的成员时,也应避免用值接收者复制它。小而独立的值,例如坐标、范围或已经设计成按值使用的配置,可以考虑值接收者。Go 官方风格建议还强调:同一个类型的方法尽量保持一致,不为了某一个“只读”方法就随意混用。上例混用是为了对照机制,不是推荐的计数器 API。[7][8]

性能上没有“指针一定更快”的通用答案。值接收者有复制成本,指针接收者有共享、间接访问和可能的逃逸代价;这些是否重要,取决于对象大小、调用方式和编译结果。先让修改语义清楚,需要优化时再基准测量。本篇没有用一组微基准替所有结构体下结论。[4:1]

方法集:判断接口实现,不能只看某次调用能否编译 ​

沿用 Value 和 Inc,去掉演示用的 TryChange 不影响下表中的规则:[9]

类型包含值接收者 Value()包含指针接收者 Inc()
Counter是否
*Counter是是

所谓方法集,就是按语言规则,一个类型具有哪些方法。接口要求的方法必须由类型的方法集满足。[9:1]

正在绘制图示…

可切换适合窗口或原始尺寸
查看图源代码
flowchart LR
    V["func Counter.Value"] --> T["Counter 的方法集"]
    V --> P["*Counter 的方法集"]
    I["func *Counter.Inc"] --> P
    T --> R["满足 Value 接口"]
    P --> R
    P --> W["满足 Inc 接口"]
放大图示

原始尺寸下可横向或纵向滚动;按 Escape 关闭。

正在绘制图示…

查看图源代码
flowchart LR
    V["func Counter.Value"] --> T["Counter 的方法集"]
    V --> P["*Counter 的方法集"]
    I["func *Counter.Inc"] --> P
    T --> R["满足 Value 接口"]
    P --> R
    P --> W["满足 Inc 接口"]

为什么 c.Inc() 能调,Counter 却不实现接口 ​

go
type Valuer interface { Value() int }
type Incrementer interface { Inc() }

var _ Valuer = Counter{}
var _ Valuer = (*Counter)(nil)
var _ Incrementer = (*Counter)(nil)
// var _ Incrementer = Counter{} // 编译失败

var _ 接口 = 值 是一个编译期实现检查,既不要求真的使用这个变量,也不会调用指针的方法。最后一行如果启用,本次编译器报:

text
Counter does not implement Incrementer (method Inc has pointer receiver)

但前面 c.Inc() 明明能运行。原因是 c 是可寻址变量,Go 允许把这种调用写成 (&c).Inc() 的简写。这个调用便利只改变如何取得接收者,没有把 Inc 加入 Counter 的方法集。向接口赋值时,判断的仍然是 Counter 自身的方法集。[10][9:2]

可寻址可以先理解为“这里有一个能取地址的具体变量”。普通局部变量、结构体变量的字段、slice 元素通常可以;map 元素、普通函数返回的临时结构体不能。因此,下面几种写法不能靠自动取址过关:[11]

go
Counter{}.Inc()
m := map[string]Counter{"x": {}}
m["x"].Inc()
_ = Counter.Inc // 这个方法表达式也不存在

要修改 map 中的值,取出可寻址的副本、调用、再放回;或者确实需要共享对象时,让 map 存 *Counter。可取址规则与方法集规则要分开判断,更多 map 的值语义见 Go 类型系统。

放进接口的值,仍然遵循复制规则 ​

go
c := Counter{N: 2}
var v Valuer = c
var p Incrementer = &c
p.Inc()
fmt.Println(v.Value(), c.Value())
text
2 3

v 保存的是当时复制进去的 Counter 值,p 保存的是指向原对象的指针。通过 p 修改原对象以后,v 中独立的整数字段不会跟着变成 3。接口没有抹掉“值还是指针”的区别;完整的动态类型与 nil 行为见 Go 接口。[6:2][12]

把方法保存成函数时,接收者何时确定 ​

方法也可以像普通函数一样传递,但下面两种写法绑定接收者的时机不同。[13]

写法名称接收者来自哪里
c.Value方法值创建时求值并保存接收者,之后调用不再传接收者
Counter.Value方法表达式得到普通函数,调用时显式传第一个参数 Counter
c.Inc方法值此处 c 可寻址,保存的是对应指针接收者
(*Counter).Inc方法表达式调用时显式传 *Counter
go
c := Counter{N: 1}
saved := c.Value
inc := c.Inc
c.N = 10
inc()
fmt.Println(saved(), c.Value())

read := Counter.Value
add := (*Counter).Inc
add(&c)
fmt.Println(read(c), (*Counter).Value(&c))
fmt.Printf("%T | %T\n", read, add)
text
1 11
12 12
func(main.Counter) int | func(*main.Counter)

saved 保存了创建时的 Counter 副本,所以后来把 c.N 改到 10,它仍读出 1。inc 保存的指针仍指向 c,执行时就在当时的 10 上加一。后面的 read(c)、add(&c) 是普通函数调用:接收者被放到第一个显式参数里,因此 %T 打印出两种函数签名。[13:1]

写回调、延迟执行或把方法交给另一层处理时,这个区别会影响结果。若希望执行时读取最新状态,就不要无意中保存一个值接收者的旧副本。

嵌入:把内部字段的方法提升出来,不会建立继承关系 ​

组合最普通的写法是给内部对象一个明确字段名:

go
type Holder struct { Counter Counter }

调用时写 h.Counter.Inc()。如果改成匿名嵌入 type Holder struct { Counter },在选择器没有冲突时,可以简写为 h.Inc()。底层仍然有一个名为 Counter 的字段;嵌入的作用之一,是提升字段和方法的可访问路径。[14]

方式调用形态主要取舍
命名字段h.Counter.Inc()依赖关系直接可见,不自动扩大外层方法集
嵌入字段h.Inc()减少转发代码,但会把符合规则的方法带到外层
显式包装方法外层定义自己的 Inc,内部再调用多写一点代码,可控制暴露哪些操作与额外规则

“组合优于继承”在这里更适合理解成一种设计建议:先把对象之间的协作写清楚,是否嵌入是下一步选择。不要仅为省几个字符嵌入一个很大的类型,结果让不想暴露的方法也成为 API 的一部分。[14:1]

外层同名方法不会改变内层方法的接收者 ​

go
type Base struct{}
func (Base) Name() string { return "base" }
func (b Base) Label() string { return b.Name() }

type Service struct { Base }
func (Service) Name() string { return "service" }

s := Service{}
fmt.Println(s.Name(), s.Label(), s.Base.Name())
text
service base base

s.Name() 选中外层自己的方法。s.Label() 提升自 Base,实际接收者仍是 s.Base;进入 Base.Label 后,b.Name() 调用的自然是 Base.Name,不会自动回到外层去找 Service.Name。嵌入没有提供这类继承式动态分派。[14:2][15]

正在绘制图示…

可切换适合窗口或原始尺寸
查看图源代码
flowchart LR
    A["s.Name()"] --> B["Service.Name<br/>接收者 Service"]
    C["s.Label()"] --> D["s.Base.Label()<br/>接收者 Base"]
    D --> E["Base.Name()<br/>返回 base"]
放大图示

原始尺寸下可横向或纵向滚动;按 Escape 关闭。

正在绘制图示…

查看图源代码
flowchart LR
    A["s.Name()"] --> B["Service.Name<br/>接收者 Service"]
    C["s.Label()"] --> D["s.Base.Label()<br/>接收者 Base"]
    D --> E["Base.Name()<br/>返回 base"]

还要留意名字冲突:如果两条同样深的嵌入路径都提供 X,s.X 会产生歧义,需要明确写 s.Left.X 或 s.Right.X。文末编译反例检查了这一点。[15:1]

嵌入 T 与嵌入 *T,会得到不同的方法集 ​

在没有冲突和遮蔽的前提下,提升规则如下。ValueEmbedded 与 PointerEmbedded 的定义都在完整代码中。[14:3]

外层定义外层值的方法集外层指针的方法集
struct { Counter }提升 Counter 的值接收者方法还提升 *Counter 的指针接收者方法
struct { *Counter }提升值接收者与指针接收者方法同样提升两类方法
go
v := ValueEmbedded{Counter: Counter{N: 3}}
v.Inc()
fmt.Println(v.Value(), v.Counter.N)

p := PointerEmbedded{Counter: &Counter{N: 5}}
var inc Incrementer = p
inc.Inc()
fmt.Println(p.Value())
text
4 4
6

第一段 v.Inc() 仍利用了可寻址变量的调用便利;ValueEmbedded 值本身不因此实现 Incrementer。第二段则不同:由于嵌入的是 *Counter,PointerEmbedded 值的方法集里就有提升后的 Inc,所以可以直接交给接口。[14:4][10:1]

代价是零值:var p PointerEmbedded 的内嵌指针是 nil,调用这里的 p.Inc() 会在访问 c.N 时 panic。指针接收者方法并非一遇 nil 就由语言禁止调用,它可以自己处理 nil;但本例 Inc 没有这种处理。选指针嵌入时,要明确初始化要求,而不是只看方法集更“大”。文末测试验证了零值调用的 panic。[10:2][15:2]

struct tag:约定给谁看,谁才会处理 ​

字段后面的标签是字符串元数据。语言会把它提供给反射,并把它纳入结构体类型身份的判定;标签本身不会执行校验、转换或数据库操作。reflect 约定的常见格式是由空格分隔的 key:"value" 对,JSON、数据库库、校验库各自读取自己关心的键。[16]

go
type User struct {
    Name   string `json:"name" validate:"required"`
    Count  int    `json:"count,omitempty"`
    Secret string `json:"-"`
    note   string
}

u := User{Name: "gopher", Secret: "hidden", note: "private"}
b, err := json.Marshal(u)
fmt.Println(string(b), err)
f, _ := reflect.TypeOf(u).FieldByName("Name")
fmt.Println(f.Tag.Get("json"), f.Tag.Get("validate"))
text
{"name":"gopher"} <nil>
name required

四个字段的处理各有原因:[17]

字段为什么这样输出
Name导出字段,json:"name" 指定 JSON 名称
Countomitempty 会省略这里值为 0 的整数
Secretjson:"-" 明确忽略
note未导出,不作为普通字段编码

validate:"required" 只是被反射读出了字符串 required,本例没有校验库消费它。把 User{} 交给 json.Marshal,仍会成功得到 {"name":""},文末测试验证了这个边界。标签写上了,不等于校验已经发生。

要区分“没这个标签”和“标签明确写了空字符串”,使用 Lookup:

go
t := reflect.StructTag(`label:""`)
a, aok := t.Lookup("label")
z, zok := t.Lookup("missing")
fmt.Printf("%q %t | %q %t\n", a, aok, z, zok)
text
"" true | "" false

Get 对两种情况都返回空串,Lookup 多返回的布尔值才保留了存在性。标签格式错误时,Get、Lookup 的结果不能依赖,包文档明确写了“未指定”。go vet 能检查常见标签格式错误;文末用 json: "n" 做了故意失败的检查。[16:1]

这里使用的是 encoding/json,不是 encoding/json/v2。复杂嵌入字段、同名标签和序列化细节应继续看对应包的字段选择规则,不能把上面四行表推广成所有嵌入情形。[17:1]

内存对齐:字段大小加起来,不一定等于结构体大小 ​

对齐可以先理解为:一个字段的起始地址要满足该类型要求的边界。为了把字段放到合适的偏移,结构体中可能插入不承载业务值的填充字节,末尾也可能补齐。unsafe.Sizeof 计算结构体自身大小时包括这些填充,但不把 slice、map、指针间接引用的数据一起算进去。[18]

看两个只有字段顺序不同的类型:

go
type Unpacked struct {
    A byte
    B int64
    C byte
}
type Packed struct {
    B int64
    A byte
    C byte
}

var a Unpacked
var b Packed
fmt.Println("unpacked:", unsafe.Sizeof(a), unsafe.Alignof(a),
    unsafe.Offsetof(a.A), unsafe.Offsetof(a.B), unsafe.Offsetof(a.C))
fmt.Println("packed:", unsafe.Sizeof(b), unsafe.Alignof(b),
    unsafe.Offsetof(b.A), unsafe.Offsetof(b.B), unsafe.Offsetof(b.C))

本次 Go 1.27.2、Linux/amd64 输出:

text
unpacked: 24 8 0 8 16
packed: 16 8 8 0 9

每行依次是总大小、对齐要求、A/B/C 三个字段的偏移。字段数据只有 1+8+1=10 字节,额外空间来自填充。这个架构上 int64 字段要按 8 字节对齐,第一种布局在 A 后面补 7 字节,C 后面又补 7 字节;第二种把 B 放在最前面,中间不必补,末尾只补 6 字节。[18:1]

正在绘制图示…

可切换适合窗口或原始尺寸
查看图源代码
flowchart LR
    subgraph U["Unpacked:24 字节"]
        U1["A<br/>偏移 0"] --> U2["填充<br/>1—7"] --> U3["B<br/>8—15"] --> U4["C<br/>16"] --> U5["填充<br/>17—23"]
    end
    subgraph P["Packed:16 字节"]
        P1["B<br/>0—7"] --> P2["A、C<br/>8、9"] --> P3["填充<br/>10—15"]
    end
放大图示

原始尺寸下可横向或纵向滚动;按 Escape 关闭。

正在绘制图示…

查看图源代码
flowchart LR
    subgraph U["Unpacked:24 字节"]
        U1["A<br/>偏移 0"] --> U2["填充<br/>1—7"] --> U3["B<br/>8—15"] --> U4["C<br/>16"] --> U5["填充<br/>17—23"]
    end
    subgraph P["Packed:16 字节"]
        P1["B<br/>0—7"] --> P2["A、C<br/>8、9"] --> P3["填充<br/>10—15"]
    end

这张图描述内存中的排列顺序,箭头不表示程序调用或指针引用。变量名 Packed 也只是为了对照,它没有使用某种取消对齐的“打包”功能。

需要看什么工具不要误读成什么
结构体自身多少字节unsafe.Sizeof(x)不是整个对象关联数据的总内存
对齐边界unsafe.Alignof(x) 或字段形式不是每个平台都固定相同
字段离结构体起点多远unsafe.Offsetof(x.Field)不是网络协议或文件格式约定

当某类对象数量很大时,减少填充可能值得;对象只有少量实例时,字段可读性可能更重要。不要为了省几个字节随意改外部契约,也不要直接把结构体的原始内存当作跨平台编码。需要稳定格式时,应按协议显式序列化。这些是根据布局依赖平台得出的工程建议,本篇只测了两个类型的静态大小,没有测真实业务的内存或速度收益。[18:2]

写一个结构体 API 时,可以按这个顺序检查:对象是一个可复制的值,还是共享的可变状态;相应方法集能否满足实际接口;嵌入是否暴露了恰当的操作;标签是否真有对应代码消费;布局是否已经重要到值得测量。这样每个选择都有具体理由,不必把所有结构体一律写成指针,也不必为模仿继承而堆嵌入层次。

复现与检查结果 ​

五份文件按下文路径保存后执行:

bash
go version
go run .
go test -count=1 -v .
go test -count=1 -race .
go vet .
go build -o invalid.a testdata/compile_fail.go
go vet testdata/vet_fail.go

本次环境为 Go 1.27.2、Linux/amd64、CGO_ENABLED=1。正常程序、4 个 Example、3 个 Test、race、vet 均通过。最后两条是故意失败检查:编译器拒绝错误的方法集和歧义选择器;vet 报出坏标签与复制 mutex。它们都返回退出码 1。布局数字不放进跨平台测试断言。race 只覆盖本次执行路径,本例没有开展并发性能实验。[19]

编译反例的核心诊断包括:

text
Counter does not implement Incrementer (method Inc has pointer receiver)
cannot call pointer method Inc on Counter
invalid method expression Counter.Inc (needs pointer receiver (*Counter).Inc)
ambiguous selector both.X

vet 的核心诊断包括:

text
struct field tag `json: "n"` not compatible with reflect.StructTag.Get: bad syntax for struct tag value
copyLock passes lock by value: sync.Mutex

诊断后面的类型内部细节、路径和行号可能随工具链变化;下面保留完整反例,便于在自己的版本重新检查。

完整可复现代码 ​

go.mod ​

go
module example.com/go-structs

go 1.27.0

main.go ​

代码块 · 131 行 · 点击收起 / 展开
go
package main

import (
	"encoding/json"
	"fmt"
	"reflect"
	"unsafe"
)

type Counter struct {
	N       int
	Samples []int
}

func (c Counter) TryChange() { c.N++; c.Samples[0]++ }
func (c Counter) Value() int { return c.N }
func (c *Counter) Inc()      { c.N++ }

type Valuer interface{ Value() int }
type Incrementer interface{ Inc() }

var _ Valuer = Counter{}
var _ Valuer = (*Counter)(nil)
var _ Incrementer = (*Counter)(nil)

func receivers() {
	c := Counter{N: 1, Samples: []int{10}}
	c.TryChange()
	fmt.Println(c.N, c.Samples)
	c.Inc()
	fmt.Println(c.N)
	var v Valuer = c
	var p Incrementer = &c
	p.Inc()
	fmt.Println(v.Value(), c.Value())
}

func methodFunctions() {
	c := Counter{N: 1}
	saved := c.Value
	inc := c.Inc
	c.N = 10
	inc()
	fmt.Println(saved(), c.Value())
	read := Counter.Value
	add := (*Counter).Inc
	add(&c)
	fmt.Println(read(c), (*Counter).Value(&c))
	fmt.Printf("%T | %T\n", read, add)
}

type Base struct{}

func (Base) Name() string    { return "base" }
func (b Base) Label() string { return b.Name() }

type Service struct{ Base }

func (Service) Name() string { return "service" }

type ValueEmbedded struct{ Counter }
type PointerEmbedded struct{ *Counter }

var _ Valuer = ValueEmbedded{}
var _ Incrementer = (*ValueEmbedded)(nil)
var _ Valuer = PointerEmbedded{}
var _ Incrementer = PointerEmbedded{}

func embedding() {
	s := Service{}
	fmt.Println(s.Name(), s.Label(), s.Base.Name())
	v := ValueEmbedded{Counter: Counter{N: 3}}
	v.Inc()
	fmt.Println(v.Value(), v.Counter.N)
	p := PointerEmbedded{Counter: &Counter{N: 5}}
	var inc Incrementer = p
	inc.Inc()
	fmt.Println(p.Value())
}

type User struct {
	Name   string `json:"name" validate:"required"`
	Count  int    `json:"count,omitempty"`
	Secret string `json:"-"`
	note   string
}

func tags() {
	u := User{Name: "gopher", Secret: "hidden", note: "private"}
	b, err := json.Marshal(u)
	fmt.Println(string(b), err)
	f, _ := reflect.TypeOf(u).FieldByName("Name")
	fmt.Println(f.Tag.Get("json"), f.Tag.Get("validate"))
	t := reflect.StructTag(`label:""`)
	a, aok := t.Lookup("label")
	z, zok := t.Lookup("missing")
	fmt.Printf("%q %t | %q %t\n", a, aok, z, zok)
}

type Unpacked struct {
	A byte
	B int64
	C byte
}
type Packed struct {
	B int64
	A byte
	C byte
}

func layout() {
	var a Unpacked
	var b Packed
	fmt.Println("unpacked:", unsafe.Sizeof(a), unsafe.Alignof(a),
		unsafe.Offsetof(a.A), unsafe.Offsetof(a.B), unsafe.Offsetof(a.C))
	fmt.Println("packed:", unsafe.Sizeof(b), unsafe.Alignof(b),
		unsafe.Offsetof(b.A), unsafe.Offsetof(b.B), unsafe.Offsetof(b.C))
}

func main() {
	for _, demo := range []struct {
		name string
		run  func()
	}{
		{"receivers", receivers}, {"method functions", methodFunctions},
		{"embedding", embedding}, {"tags", tags}, {"layout", layout},
	} {
		fmt.Println("==", demo.name, "==")
		demo.run()
	}
}

main_test.go ​

代码块 · 58 行 · 点击收起 / 展开
go
package main

import (
	"encoding/json"
	"testing"
)

func Example_receivers() {
	receivers()
	// Output:
	// 1 [11]
	// 2
	// 2 3
}
func Example_methodFunctions() {
	methodFunctions()
	// Output:
	// 1 11
	// 12 12
	// func(main.Counter) int | func(*main.Counter)
}
func Example_embedding() {
	embedding()
	// Output:
	// service base base
	// 4 4
	// 6
}
func Example_tags() {
	tags()
	// Output:
	// {"name":"gopher"} <nil>
	// name required
	// "" true | "" false
}
func TestNilEmbeddedPointer(t *testing.T) {
	var p PointerEmbedded
	defer func() {
		if recover() == nil {
			t.Fatal("expected nil dereference")
		}
	}()
	p.Inc()
}
func TestTagDoesNotValidate(t *testing.T) {
	b, err := json.Marshal(User{})
	if err != nil || string(b) != `{"name":""}` {
		t.Fatalf("got %s, %v", b, err)
	}
}
func TestNamedComposition(t *testing.T) {
	type Holder struct{ Counter Counter }
	h := Holder{Counter: Counter{N: 2}}
	h.Counter.Inc()
	if h.Counter.Value() != 3 {
		t.Fatal("named field not updated")
	}
}

testdata/compile_fail.go ​

go
package fail

type Counter struct{ N int }

func (c *Counter) Inc() { c.N++ }

type Incrementer interface{ Inc() }

var _ Incrementer = Counter{}

type Left struct{ X int }
type Right struct{ X int }
type Both struct {
	Left
	Right
}

func invalid() {
	Counter{}.Inc()
	m := map[string]Counter{"x": {}}
	m["x"].Inc()
	_ = Counter.Inc
	var both Both
	_ = both.X
}

testdata/vet_fail.go ​

go
package fail

import "sync"

type BadTag struct {
	N int `json: "n"`
}

func copyLock(mu sync.Mutex) { mu.Lock(); mu.Unlock() }

依据 ​

语言规则与本次 Go 1.27.2 官方发行包中的 doc/go_spec.html 核对;标准库行为另用同版本 go doc 检查。实测布局仅描述 Linux/amd64。


  1. Go 规范:结构体类型、复合字面量。 ↩︎

  2. Go 规范:导出标识符。 ↩︎

  3. Go 规范:零值、new、取址。 ↩︎

  4. Go FAQ:如何知道变量分配在栈还是堆。存储位置与逃逸和实现有关,不由源代码中的 new 或 & 单独决定。 ↩︎ ↩︎

  5. Go 规范:方法声明。 ↩︎ ↩︎

  6. Go 规范:值的表示、调用、赋值。 ↩︎ ↩︎ ↩︎

  7. Go 官方 Wiki:Receiver Type,给出接收者选择和保持一致的建议;建议与语言允许的范围不同。 ↩︎

  8. sync.Mutex:首次使用后不得复制;go vet 的 copylocks 与 structtag 检查。 ↩︎

  9. Go 规范:方法集、实现接口。 ↩︎ ↩︎ ↩︎

  10. Go 规范:调用:当接收者可寻址且其指针方法集包含目标方法时,可以简写调用。 ↩︎ ↩︎ ↩︎

  11. Go 规范:取址操作、索引表达式。 ↩︎

  12. Go 规范:变量的静态与动态类型、接口类型。 ↩︎

  13. Go 规范:方法表达式、方法值。方法值创建时求值并保存接收者。 ↩︎ ↩︎

  14. Go 规范:结构体嵌入及提升的方法集;Effective Go 的嵌入说明,解释接收者仍是内部对象,组合不同于继承。 ↩︎ ↩︎ ↩︎ ↩︎ ↩︎

  15. Go 规范:选择器,按最浅且唯一的字段或方法解析;nil 指针字段访问会 panic。 ↩︎ ↩︎ ↩︎

  16. Go 规范:结构体标签;reflect.StructTag、Get、Lookup,定义标签约定及错误格式的未指定结果。 ↩︎ ↩︎

  17. encoding/json.Marshal:导出字段、字段标签、omitempty、忽略字段及嵌入字段选择规则。这里引用 v1 包的约定。 ↩︎ ↩︎

  18. unsafe.Sizeof、Alignof、Offsetof;Go 规范:大小与对齐保证。具体字段布局由本文 Go 1.27.2 实测补充,不把 amd64 数字推广到其他架构。 ↩︎ ↩︎ ↩︎

  19. Go 数据竞争检测器:支持环境、命令和覆盖限制。 ↩︎

保留各书来源与许可 · 示例运行条件以正文为准